﻿using UnityEngine;
using UnityEngine.Rendering;

public partial class PostFXStack
{
    private const string bufferName = "Post FX";

    private const int maxBloomPyramidLevels = 16;

    private readonly int
        bloomBucibicUpsamplingId = Shader.PropertyToID("_BloomBicubicUpsampling");

    private readonly int
        bloomIntensityId = Shader.PropertyToID("_BloomIntensity");

    private readonly int
        bloomPrefilterId = Shader.PropertyToID("_BloomPrefilter");

    private readonly int
        bloomResultId = Shader.PropertyToID("_BloomResult");

    private readonly int
        bloomThresholdId = Shader.PropertyToID("_BloomThreshold");

    private readonly int
        fxSourceId = Shader.PropertyToID("_PostFXSource");

    private readonly int
        fxSource2Id = Shader.PropertyToID("_PostFXSource2");

    private readonly int bloomPyramidId;

    private readonly CommandBuffer buffer = new CommandBuffer
    {
        name = bufferName
    };

    private Camera camera;

    private ScriptableRenderContext context;
    private PostFXSettings settings;

    public PostFXStack()
    {
        bloomPyramidId = Shader.PropertyToID("_BloomPyramid0");
        for (var i = 1; i < maxBloomPyramidLevels * 2; i++) Shader.PropertyToID("_BloomPyramid" + i);
    }

    public bool IsActive => settings != null;

    private bool useHDR;

    public void Setup
        (ScriptableRenderContext context, Camera camera, PostFXSettings settings, bool useHDR)
    {
        this.useHDR = useHDR;

        this.context = context;
        this.camera = camera;
        this.settings =
            camera.cameraType <= CameraType.SceneView ? settings : null;
        ApplySceneViewState();
    }

    public void Render(int sourceId)
    {
        if (DoBloom(sourceId))
        {
            DoToneMapping(bloomResultId);
            buffer.ReleaseTemporaryRT(bloomResultId);
        }
        else
            DoToneMapping(sourceId);

        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();
    }


    private bool DoBloom(int sourceId)
    {
        var bloom = settings.Bloom;
        int width = camera.pixelWidth / 2, height = camera.pixelHeight / 2;

        if (bloom.maxIterations == 0 || bloom.intensity <= 0f ||
            height < bloom.downscaleLimit * 2 || width < bloom.downscaleLimit * 2)
        {
            return false;
        }

        buffer.BeginSample("Bloom");

        Vector4 threshold;
        threshold.x = Mathf.GammaToLinearSpace(bloom.threshold);
        threshold.y = threshold.x * bloom.thresholdKnee;
        threshold.z = 2f * threshold.y;
        threshold.w = 0.25f / (threshold.y + 0.00001f);
        threshold.y -= threshold.x;
        buffer.SetGlobalVector(bloomThresholdId, threshold);

        var format = useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
        buffer.GetTemporaryRT(
            bloomPrefilterId, width, height, 0, FilterMode.Bilinear, format
        );
        Draw(sourceId, bloomPrefilterId, bloom.fadeFireflies ? Passes.BloomPrefilterFireFlies : Passes.BloomPrefilter);
        width /= 2;
        height /= 2;

        int fromId = bloomPrefilterId, toId = bloomPyramidId + 1;
        int i;
        for (i = 0; i < bloom.maxIterations; i++)
        {
            if (height < bloom.downscaleLimit || width < bloom.downscaleLimit)
                break;
            var midId = toId - 1;
            buffer.GetTemporaryRT(
                midId, width, height, 0, FilterMode.Bilinear, format);

            buffer.GetTemporaryRT(
                toId, width, height, 0, FilterMode.Bilinear, format);
            Draw(fromId, midId, Passes.BloomHorizontal);
            Draw(midId, toId, Passes.BloomVertical);
            fromId = toId;
            toId += 2;
            width /= 2;
            height /= 2;
        }

        buffer.ReleaseTemporaryRT(bloomPrefilterId);
        buffer.SetGlobalFloat(
            bloomBucibicUpsamplingId, bloom.bicubicUpsampling ? 1f : 0f);
        Passes combinePass, finalPass;
        float finalIntensity;
        if (bloom.mode == PostFXSettings.BloomSettings.Mode.Additive)
        {
            combinePass = finalPass = Passes.BloomAdd;
            buffer.SetGlobalFloat(bloomIntensityId, 1f);
            finalIntensity = bloom.intensity;
        }
        else
        {
            combinePass = Passes.BloomScatter;
            finalPass = Passes.BloomScatterFinal;
            buffer.SetGlobalFloat(bloomIntensityId, bloom.scatter);
            finalIntensity = Mathf.Min(bloom.intensity, 0.95f);
        }

        if (i > 1)
        {
            buffer.ReleaseTemporaryRT(fromId - 1);
            toId -= 5;
            for (i -= 1; i > 0; i--)
            {
                buffer.SetGlobalTexture(fxSource2Id, toId + 1);
                Draw(fromId, toId, combinePass);
                buffer.ReleaseTemporaryRT(fromId);
                buffer.ReleaseTemporaryRT(toId + 1);
                fromId = toId;
                toId -= 2;
            }
        }
        else
        {
            buffer.ReleaseTemporaryRT(bloomPyramidId);
        }

        buffer.SetGlobalFloat(bloomIntensityId, finalIntensity);
        buffer.SetGlobalTexture(fxSource2Id, sourceId);
        buffer.GetTemporaryRT(bloomResultId, camera.pixelWidth,
            camera.pixelHeight, 0, FilterMode.Bilinear, format);

        Draw(fromId, bloomResultId, finalPass);
        buffer.ReleaseTemporaryRT(fromId);
        buffer.EndSample("Bloom");
        return true;
    }

    void DoToneMapping(int sourceID)
    {
        var mode = settings.ToneMapping.mode;
        var pass = mode < 0 ? Passes.Copy : Passes.ToneMappingReinhard + (int) mode;
        Draw(sourceID, BuiltinRenderTextureType.CameraTarget, pass);
    }

    private void Draw(RenderTargetIdentifier from, RenderTargetIdentifier to, Passes pass)
    {
        buffer.SetGlobalTexture(fxSourceId, from);
        buffer.SetRenderTarget(
            to, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
        buffer.DrawProcedural(
            Matrix4x4.identity, settings.Material, (int) pass,
            MeshTopology.Triangles, 3);
    }

    private enum Passes
    {
        BloomAdd,
        BloomHorizontal,
        BloomPrefilter,
        BloomPrefilterFireFlies,
        BloomScatter,
        BloomScatterFinal,
        BloomVertical,
        Copy,
        ToneMappingReinhard,
        ToneMappingNeutral,
        ToneMappingACES,
    }
}